#!/usr/bin/python
#
# Copyright 2010 Google Inc. All Rights Reserved.

"""The Breadcrumb parser + compiler.

Requirements:
 * PyParsing 1.5 (http://pypi.python.org/pypi/pyparsing)
 * Markdown 2.0 (http://pypi.python.org/pypi/Markdown)
"""

__author__ = 'blinks@google.com (Adam Blinkinsop)'

from pyparsing import *
from xml.dom import minidom

import logging
import markdown
import re
import simplejson as json
import sys

impl = minidom.getDOMImplementation()


def main(*args, **opts):
  """Compile Breadcrumb source files into HTML output."""
  # Create a parser.
  parser = Parser()

  # Parse each filename in turn.
  for filename in args:
    # Open the file, create the output filename.
    if filename != '-':
      f = open(filename, 'r')
      out = filename + '.html'
    else:
      f = sys.stdin
      out = 'out.html'

    # Perform the actual compilation.
    try:
      code = f.read()
      pages = parser.Parse(code)
    finally:
      f.close()

    f = open(out, 'w')
    try:
      f.write(TEMPLATE % {'story': json.dumps({'pages': pages})})
      print 'Compiled %r to %r.' % (filename, out)
    finally:
      f.close()


def BuildPage(s, l, t):
  """Create a page as a parse action."""
  logging.debug('BuildPage(%r, %r, %r)', s, l, t)
  _, key, script = t[0]
  t[1] = "p.push('<p>');%s;p.push('</p>')" % t[1].strip()
  if script:
    t[1] = script + ';' + t[1]
  return {'id': key, '_value': t[1]}


def BuildLink(s, l, t):
  """Create a link as a parse action."""
  logging.debug('BuildLink(%r, %r, %r)', s, l, t)
  text, hrefs = t[0], t[1:]
  if not hrefs:
    return '[%s]' % text

  doc = impl.createDocument(None, None, None)
  a = doc.createElement('a')
  try:
    dom = minidom.parseString(u'<p>%s</p>' % text)
    dom = dom.documentElement.childNodes[0]
  except Exception, err:
    dom = doc.createTextNode(text)
  a.appendChild(dom)
  if len(hrefs) == 1:
    _, key, script = hrefs[0]
    if key.startswith('http:') or key.startswith('https:'):
      a.setAttribute('href', key)
    elif key.startswith('javascript'):
      a.setAttribute('href', key)
    elif script:
      a.setAttribute('href', 'javascript:%s;go(%r)' % (script, str(key)))
    else:
      a.setAttribute('href', 'javascript:go(%r)' % str(key))

  else:
    def Script(key, script):
      if key in ['http', 'https']:
        return 'window.location = %r' % str(key + ':' + script)
      elif key in ['javascript', '?']:
        return script
      elif script:
        return script + ';go(%r)' % str(key)
      else:
        return 'go(%r)' % str(key)
    a.setAttribute('href', 'javascript:choose(%s)'
          % ','.join('function(){%s}' % Script(key, script)
                     for _, key, script in hrefs))
  return a.toxml()


def BuildText(s, l, t):
  logging.debug('BuildText(%r, %r, %r)', s, l, t)
  t = u''.join(unicode(x) for x in t)
  if not t.strip():
    return None
  else:
    text = markdown.markdown(t)
    if text.startswith('<p>') and text.endswith('</p>'):
      text = text[3:-4]
    text = re.sub(r'\s+', ' ', text)
    if re.search(r'^\s+', t):
      text = u' ' + text
    if re.search(r'\s+$', t):
      text = text + u' '
      
    return u'p.push(%r)' % text.encode('ascii', 'xmlcharrefreplace')


class Parser(object):
  """A parser for Breadcrumb stories."""

  def Parse(self, text):
    logging.debug('Parser.Parse(%r)', text)
    try:
      results = []
      pages = re.split(r'\n+\s*(?=\()', text)
      for page in pages:
        logging.debug('page == %r', page)
        if not page.strip():
          continue
        ast = Parser.page.parseString(page.strip())
        results.append(ast[0])
        logging.debug('results == %r', results)
      return results
    except ParseException, err:
      # Log the error, with help text (for debugging, not a program fault).
      logging.error('%s:\n%s\n%s', err, err.line, ' ' * (err.column - 1) + '^')
      # Return the error to the user.
      return [{'id': 'error', '_value': ';'.join([
          'p.push("<p class=\'error\'>I found a problem while proofreading: ")',
          'p.push("<pre>")',
          'p.push(%r)' % str(err.line) + '\n',
          'p.push(%r)' % (' ' * (err.column - 1) + '^\n'),
          'p.push("</pre>")',
          'p.push(%r)' % str(err),
          'p.push("</p>")',
          ])}]

  id = Word(alphanums + '$_')

  script = Forward()
  LPAREN, RPAREN = Suppress('('), Suppress(')')
  href = (LPAREN + (Regex(r'(?:https?|javascript)://[^)]+') ^ id)
          + Optional(Suppress(':') + script, default='') + RPAREN)
  href.setParseAction(lambda s, p, t: ('href', t[0], t[1]))

  text = Regex(r'[^[<]+|</?\w.*?>|<(?!%)')
  LBRACK, RBRACK = Suppress('['), Suppress(']')
  link = (LBRACK + Regex(r'[^]]+') + RBRACK
          + Regex('\s*').suppress() + OneOrMore(href))
  link.setParseAction(BuildLink)

  # Script is executed or evaluated.
  script << Combine(
      ZeroOrMore(Regex('([^()/\'"%]|%(?!>))+').setName('boring chars')
                 ^ ('(' + script + ')').setName('parenthetical')
                 ^ cStyleComment
                 ^ quotedString)).setName('script')

  LESC, RESC = Suppress('<%'), Suppress('%>')
  escaped = LESC + Optional('=', default=False) + script + RESC
  escaped.setName('script')
  escaped.setParseAction(lambda s, l, t: (t[0] and 'p.push(%s)' or '%s')
                         % str(t[1]).strip())

  markdown = OneOrMore(text | link)
  markdown.setParseAction(BuildText)

  body = Combine(OneOrMore(escaped ^ markdown), joinString=';')

  page = href + body + StringEnd()
  page.setParseAction(BuildPage)


TEMPLATE = """
<html> 
<head> 
<script src="http://www.google.com/jsapi"></script> 
<script> 
  var story, state = {};
  google.load("jquery", "1");
  google.setOnLoadCallback(function() {
    story = %(story)s;
 
    story.cache = {}
    // Compile the pages in the story.
    for (i in story.pages) {
      var body = ("var p = []; with(state) {"
        + story.pages[i]._value + "} return p.join('');");
      try {
        story.cache[story.pages[i].id] = new Function('state', body);
      } catch (e) {
        alert(e);
      }
    }
    go(story.pages[0].id);
  });
 
  function choose() {  // Call one of the arguments at random.
    if (arguments.length == 0) {
      alert('No arguments passed to choose().');
    } else {
      choice = Math.floor(Math.random() * arguments.length);
      arguments[choice]();
    }
  }
 
  function go(key) {
    $('#view').fadeOut('fast', function() {
      try {
        var fn = story.cache[key];
        var html;
        if (fn != undefined) {
          html = fn(state);
          if (story.cache['Status']) {
            html += '<hr />' + story.cache['Status'](state);
          }
        } else {
          html = '<p>There is no page ' + key + '.</p>';
        }
        $('#view').html(html);
      } catch (e) {
        $('#view').html('<p>Error at ' + key + ': ' + e + '</p>');
      } finally {
        $('#view').fadeIn('fast');
      }
    });
  }
</script> 
</head> 
<body><div id="view"><p>Just a second...</p></div></body> 
</html> 
"""


def Flags():
  from optparse import OptionParser
  parser = OptionParser(
      version='bcc 1.0',
      usage='usage: %prog [options] script1 [script2 [...]]',
      description='A Breadcrumb story compiler.',
      epilog='See https://code.google.com/p/google-breadcrumb/ for details.')
  opts, args = parser.parse_args()
  if not args:
    parser.error('You must provide a script filename (or "-" for stdin).')
  return opts, args


if __name__ == '__main__':
  opts, args = Flags()
  main(*args, **opts.__dict__)
